﻿/**
 * Fenozix
 *
 * @author		Allen Chou
 * @version		1.0.2 (last update: Dec 3 2008)
 * @link        http://cjcat.blogspot.com
 * @link		http://cjcat2266.deviantart.com
 */

package idv.cjcat.physics.fenozix {
	import flash.errors.IllegalOperationError;
	import flash.events.Event;
	
	import idv.cjcat.*;
	import idv.cjcat.geom.*;

	/**
	 * Fenozix is a physics engine for simulating simple 2D particle physics.
	 * <p>
	 * All objects are regarded as a particle without area/volume.
	 * </p>
	 * 
	 * <p>
	 * Here's a sample workflow of using Fenizix:
	 * </p>
	 * 
	 * <p>
	 * <ul>
	 * <li>Instantiate an engine object, namely, a <code>Fenozix</code> object.</li>
	 * <li>You can add whatever object with defined <code>x</code> and <code>y</code> properties to simulation through the <code>add()</code> method.</li>
	 * <li>You can add spring constraints between objects through the <code>crateSpring()</code> method.</li>
	 * <li>Repeatedly call the <code>step()</code> method to keep the physics simulation going.</li>
	 * </ul>
	 * </p>
	 */
	
	 /**
	  * @example
	  * <listing>
	  * //This examples creates a spring chain with its head following the mouse cursor.
	  *
	  * import idv.cjcat.display.LineStyle;
	  * import idv.cjcat.display.dynashape.DynaGraphics;
	  * import idv.cjcat.display.dynashape.DynaShape;
	  * import idv.cjcat.display.shapes.Circle;
	  * import idv.cjcat.physics.fenozix.Fenozix;
	  *
	  * var circles:Array = new Array();
	  * var i:int;
	  * for (i = 0; i &lt; 5; i++) {
	  * 	var c:Circle = new Circle(10);
	  * 	c.y = 20 &#42; i;
	  * 	circles.push(c);
	  * 	addChild(c);
	  * }
	  *
	  * //Creates the simulation environment
	  * var engine:Fenozix = new Fenozix();
	  * engine.gravity.y = 0.5;
	  * engine.add(circles[0], 0); //static object
	  * for (i = 1; i &lt; circles.length; i++) engine.add(circles[i]);
	  * for (i = 0; i &lt; circles.length - 1; i++) engine.createSpring(circles[i], circles[i + 1]);
	  * 
	  * //Creates a DynaShape for spring constarints visualization
	  * var ds:DynaShape = new DynaShape();
	  * var dg:DynaGraphics = new DynaGraphics();
	  * dg.lineStyle(new LineStyle());
	  * dg.moveTo(circles[0]);
	  * for (i = 1; i &lt; circles.length; i++) dg.lineTo(circles[i]);
	  * ds.add(dg);
	  * addChildAt(ds, 0);
	  *
	  * //The main loop
	  * addEventListener(Event.ENTER_FRAME, loop);
	  * function loop(e:Event):void {
	  * 	circles[0].x = mouseX;
	  * 	circles[0].y = mouseY;
	  * 	engine.step();
	  * 	ds.render();
	  * }</listing>
	  */
	public class Fenozix {
		
		public var damping:Number;
		private var _gravity:Vector2 = new Vector2(0, 0);
		private var _wind:Vector2 = new Vector2(0, 0);
		
		private var _objectData:Vector.<ObjectData> = new Vector.<ObjectData>();
		private var _constraints:Vector.<AbstractConstraint> = new Vector.<AbstractConstraint>();
		private var _stepInterval:Number;
		private var _iterations:uint = 1;
		
		/**
		 * Creates a Fenozix engine.
		 * @param damping       The damping coefficient of the engine.
		 * @param stepInverval  The time step interval used for each step() call.
		 */
		public function Fenozix(damping:Number = 0.03, stepInterval:Number = 1) {
			this.damping = damping;
			this.stepInterval = stepInterval;
		}
		
		/**
		 * Adds an object to simulation.
		 * @param	obj     The object.
		 * @param	mass    The mass of the object. Setting mass to zero makes the object static; that is, the object will not be moved by forces.
		 * @param	initVX  The initial X component of the object's velocity.
		 * @param	initVY  The initial Y component of the object's velocity.
		 */
		public function add(obj:Object, mass:Number = 1, initVX:Number = 0, initVY:Number = 0):void {
			var index:int = -1;
			for each (var data:ObjectData in _objectData) {
				if (data.obj === obj) {
					index = _objectData.indexOf(data);
					break;
				}
			}
			if (index == -1) {
				_objectData.push(new ObjectData(obj, mass, initVX, initVY));
			} else {
				_objectData[index].mass = mass;
				_objectData[index].vx = initVX;
				_objectData[index].vy = initVY;
			}
		}
		
		/**
		 * Removes an object from simulation.
		 * @param	obj  The object.
		 */
		public function remove(obj:Object):void {
			for each (var data:ObjectData in _objectData) {
				if (data.obj == obj) _objectData.splice(_objectData.indexOf(data), 1);
			}
		}
		
		/**
		 * Removes all objects from simulation.
		 */
		public function clear():void {
			_objectData = new Vector.<ObjectData>();
		}
		
		/**
		 * Create a spring constraint between two objects.
		 * @param	obj1          The first object.
		 * @param	obj2          The second object.
		 * @param	stiffness     The stiffness of the spring. This value should lie between 0 and 1.
		 * @param	length        The length of the spring. By default, this value is set to the distance between the two objects.
		 * @return                A reference to the spring constraint created. This reference can be passed to the <code>removeConstraint()</code> method to remove the constraint.
		 */
		public function createSpring(obj1:Object, obj2:Object, stiffness:Number = 0.05, length:Number = NaN):Spring {
			var index1:int = -1;
			var index2:int = -1;
			
			for (var i:int = 0; i < _objectData.length; i++) {
				if (_objectData[i].obj === obj1) index1 = i;
				if (_objectData[i].obj === obj2) index2 = i;
				if (index1 != -1 && index2 != -1) break;
			}
			if (index1 == -1 || index2 == -1) throw new IllegalOperationError("More than one objects are not added to the engine through add().");
			
			var spring:Spring = new Spring(_objectData[index1], _objectData[index2], stiffness, length); 
			_constraints.push(spring);
			
			return spring;
		}
		
		/**
		 * Remove a constraint from simulation.
		 * @param	constraint  The constraint.
		 */
		public function removeConstraint(constraint:AbstractConstraint):void {
			if (_constraints.indexOf(constraint) != -1) _constraints.splice(_constraints.indexOf(constraint), 1);
		}
		
		/**
		 * Removes all constraints.
		 */
		public function clearConstraints():void {
			_constraints = new Vector.<AbstractConstraint>();
		}
		
		/**
		 * Call this method repeatedly to keep the simulation running.
		 * <p>This method can be used as an event listener.</p>
		 */
		public function step(e:Event = null):void {
			for (var i:uint = 0; i < _iterations; i++) {
				for each (var constraint:AbstractConstraint in _constraints) constraint.step(_stepInterval / _iterations);
				for each (var objData:ObjectData in _objectData) {
					if (objData.mass != 0) {
						objData.vx += gravity.x / _iterations;
						objData.vy += gravity.y / _iterations;
						objData.vx += wind.x / _iterations / objData.mass;
						objData.vy += wind.y / _iterations / objData.mass;
					}
					objData.vx *= 1 - (damping / _iterations);
					objData.vy *= 1 - (damping / _iterations);
					objData.obj.x += objData.vx * _stepInterval / _iterations;
					objData.obj.y += objData.vy * _stepInterval / _iterations;
				}
			}
		}
		
		/**
		 * The gravity vector.
		 * Applies equal acceleration to all objects.
		 */
		public function get gravity():Vector2 { return _gravity; }
		
		/**
		 * The wind vector.
		 * Applies equal force to all objects.
		 */
		public function get wind():Vector2 { return _wind; }
		
		/**
		 * How much time a single call to step() elapses.
		 */
		public function get stepInterval():Number { return _stepInterval; }
		public function set stepInterval(value:Number):void {
			_stepInterval = CJMath.clamp(value, Number.MIN_VALUE, Number.MAX_VALUE);
		}
		
		/**
		 * Iterations of each simulation step.
		 */
		public function get iterations():uint { return _iterations; }
		public function set iterations(value:uint):void {
			if (value == 0) value = 1;
			_iterations = value;
		}
	}
}